جاوااسکریپت ناهمزمان را با توابع مولد تسلط پیدا کنید. تکنیکهای پیشرفته برای ترکیب و هماهنگسازی چندین مولد برای گردشهای کاری ناهمزمان تمیزتر و قابل مدیریتتر را بیاموزید.
ترکیب ناهمزمان تابع مولد جاوااسکریپت: هماهنگی چند مولد
توابع مولد جاوااسکریپت یک مکانیسم قدرتمند برای رسیدگی به عملیات ناهمزمان به روشی که بیشتر شبیه به همزمان است، فراهم می کنند. در حالی که استفاده اصلی از مولدها به خوبی مستند شده است، پتانسیل واقعی آنها در توانایی ترکیب و هماهنگ سازی آنها نهفته است، به خصوص هنگامی که با چندین جریان داده ناهمزمان سروکار دارید. این پست به بررسی تکنیک های پیشرفته برای دستیابی به هماهنگی چند مولد با استفاده از ترکیبات async می پردازد.
درک توابع مولد
قبل از اینکه وارد ترکیب شویم، بیایید به سرعت مروری بر آنچه توابع مولد هستند و نحوه عملکرد آنها داشته باشیم.
یک تابع مولد با استفاده از دستور function* اعلام می شود. بر خلاف توابع معمولی، توابع مولد را می توان در حین اجرا متوقف و از سر گرفت. کلمه کلیدی yield برای متوقف کردن تابع و برگرداندن یک مقدار استفاده می شود. هنگامی که مولد از سر گرفته می شود (با استفاده از next())، اجرا از جایی که متوقف شده بود ادامه می یابد.
در اینجا یک مثال ساده آورده شده است:
function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}
const generator = numberGenerator();
console.log(generator.next()); // Output: { value: 1, done: false }
console.log(generator.next()); // Output: { value: 2, done: false }
console.log(generator.next()); // Output: { value: 3, done: false }
console.log(generator.next()); // Output: { value: undefined, done: true }
مولدهای ناهمزمان
برای رسیدگی به عملیات ناهمزمان، می توانیم از مولدهای ناهمزمان استفاده کنیم که با استفاده از دستور async function* اعلام می شوند. این مولدها می توانند منتظر promise ها باشند و به کد ناهمزمان اجازه می دهند تا به سبکی خطی تر و خواناتر نوشته شود.
مثال:
async function* fetchUsers(userIds) {
for (const userId of userIds) {
const response = await fetch(`https://api.example.com/users/${userId}`);
const user = await response.json();
yield user;
}
}
async function main() {
const userIds = [1, 2, 3];
const userGenerator = fetchUsers(userIds);
for await (const user of userGenerator) {
console.log(user);
}
}
main();
در این مثال، fetchUsers یک مولد ناهمزمان است که داده های کاربر را از یک API برای هر userId ارائه شده واکشی می کند. حلقه for await...of برای تکرار مولد ناهمزمان استفاده می شود و منتظر هر مقدار بازده شده قبل از پردازش آن است.
نیاز به هماهنگی چند مولد
اغلب، برنامه ها نیاز به هماهنگی بین چندین منبع داده ناهمزمان یا مراحل پردازش دارند. به عنوان مثال، ممکن است لازم باشد:
- داده ها را از چندین API به طور همزمان واکشی کنید.
- داده ها را از طریق یک سری تبدیل ها پردازش کنید که هر کدام توسط یک مولد جداگانه انجام می شوند.
- خطاها و استثناها را در چندین عملیات ناهمزمان مدیریت کنید.
- منطق جریان کنترل پیچیده، مانند اجرای شرطی یا الگوهای فنآوت/فناین، را پیادهسازی کنید.
تکنیک های سنتی برنامه نویسی ناهمزمان، مانند تماسهای برگشتی یا Promise ها، می توانند در این سناریوها مدیریت دشواری داشته باشند. توابع مولد یک رویکرد ساختاریافته تر و قابل ترکیب ارائه می دهند.
تکنیک های هماهنگی چند مولد
در اینجا چندین تکنیک برای هماهنگی چندین تابع مولد وجود دارد:
1. ترکیب مولد با `yield*`
کلمه کلیدی yield* به شما امکان می دهد به یک تکرارگر یا تابع مولد دیگر تفویض اختیار دهید. این یک بلوک ساختمانی اساسی برای ترکیب مولدها است. این به طور موثر خروجی مولد تفویض شده را در جریان خروجی مولد فعلی «صاف» می کند.
مثال:
async function* generatorA() {
yield 1;
yield 2;
}
async function* generatorB() {
yield 3;
yield 4;
}
async function* combinedGenerator() {
yield* generatorA();
yield* generatorB();
}
async function main() {
for await (const value of combinedGenerator()) {
console.log(value); // Output: 1, 2, 3, 4
}
}
main();
در این مثال، combinedGenerator تمام مقادیر را از generatorA و سپس تمام مقادیر را از generatorB بازده می دهد. این یک شکل ساده از ترکیب متوالی است.
2. اجرای همزمان با `Promise.all`
برای اجرای همزمان چندین مولد، می توانید آنها را در Promise ها قرار دهید و از Promise.all استفاده کنید. این به شما امکان می دهد داده ها را از چندین منبع به صورت موازی واکشی کنید و عملکرد را بهبود بخشید.
مثال:
async function* fetchUserData(userId) {
const response = await fetch(`https://api.example.com/users/${userId}`);
const user = await response.json();
yield user;
}
async function* fetchPosts(userId) {
const response = await fetch(`https://api.example.com/users/${userId}/posts`);
const posts = await response.json();
for (const post of posts) {
yield post;
}
}
async function* combinedGenerator(userId) {
const userDataPromise = fetchUserData(userId).next();
const postsPromise = fetchPosts(userId).next();
const [userDataResult, postsResult] = await Promise.all([userDataPromise, postsPromise]);
if (userDataResult.value) {
yield { type: 'user', data: userDataResult.value };
}
if (postsResult.value) {
yield { type: 'posts', data: postsResult.value };
}
}
async function main() {
for await (const item of combinedGenerator(1)) {
console.log(item);
}
}
main();
در این مثال، combinedGenerator داده های کاربر و پست ها را به طور همزمان با استفاده از Promise.all واکشی می کند. سپس نتایج را به عنوان اشیاء جداگانه با یک ویژگی type برای نشان دادن منبع داده بازده می دهد.
ملاحظات مهم: استفاده از `.next()` در یک مولد قبل از تکرار با `for await...of`، تکرارگر را *یک بار* پیش میبرد. این در هنگام استفاده از `Promise.all` در ترکیب با مولدها بسیار مهم است، زیرا اجرای مولد را پیشگیرانه آغاز می کند.
3. الگوهای فنآوت/فناین
الگوی fan-out/fan-in یک الگوی رایج برای توزیع کار در چندین کارگر و سپس تجمیع نتایج است. توابع مولد می توانند برای پیاده سازی این الگو به طور موثر استفاده شوند.
فنآوت: توزیع وظایف به چندین مولد.
فناین: جمع آوری نتایج از چندین مولد.
مثال:
async function* worker(taskId) {
// Simulate asynchronous work
await new Promise(resolve => setTimeout(resolve, Math.random() * 1000));
yield { taskId, result: `Result for task ${taskId}` };
}
async function* fanOut(taskIds, numWorkers) {
const workerGenerators = [];
for (let i = 0; i < numWorkers; i++) {
workerGenerators.push(worker(taskIds[i % taskIds.length])); // Round-robin assignment
}
for (let i = 0; i < taskIds.length; i++) {
yield* workerGenerators[i % numWorkers];
}
}
async function main() {
const taskIds = [1, 2, 3, 4, 5, 6, 7, 8];
const numWorkers = 3;
for await (const result of fanOut(taskIds, numWorkers)) {
console.log(result);
}
}
main();
در این مثال، fanOut وظایف (شبیه سازی شده توسط worker) را در تعداد مشخصی از کارگران توزیع می کند. اختصاص round-robin توزیع نسبتاً یکنواختی از کار را تضمین می کند. سپس نتایج از مولد fanOut بازده می شوند. توجه داشته باشید که در این مثال ساده، کارگران واقعاً به طور همزمان اجرا نمی شوند. `yield*` اجرای متوالی را در داخل `fanOut` مجبور می کند.
4. ارسال پیام بین مولدها
مولدها می توانند با ارسال مقادیر به عقب و جلو با استفاده از روش next() با یکدیگر ارتباط برقرار کنند. وقتی next(value) را در یک مولد فراخوانی می کنید، value به عبارت yield داخل مولد منتقل می شود.
مثال:
async function* producer() {
let message = 'Initial Message';
while (true) {
const received = yield message;
console.log(`Producer received: ${received}`);
message = `Producer's response to: ${received}`;
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate some work
}
}
async function* consumer(producerGenerator) {
let message = 'Consumer starting';
let result = await producerGenerator.next();
console.log(`Consumer received from producer: ${result.value}`);
while (!result.done) {
const response = `Consumer's message: ${message}`; // Create a response
result = await producerGenerator.next(response); // Send message to producer
if (!result.done) {
console.log(`Consumer received from producer: ${result.value}`); // log the response from the producer
}
message = `Next consumer message`; // Create next message to send on next iteration
await new Promise(resolve => setTimeout(resolve, 500)); // Simulate some work
}
}
async function main() {
const prod = producer();
await consumer(prod);
}
main();
در این مثال، consumer با استفاده از producerGenerator.next(response) پیامها را به producer ارسال میکند و producer این پیامها را با استفاده از عبارت yield دریافت میکند. این امکان ارتباط دو طرفه بین مولدها را فراهم می کند.
5. مدیریت خطا
مدیریت خطا در ترکیب مولد ناهمزمان نیازمند بررسی دقیق است. می توانید از بلوک های try...catch در مولدها برای مدیریت خطاهایی که در طول عملیات ناهمزمان رخ می دهند استفاده کنید.
مثال:
async function* safeFetch(url) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
yield data;
} catch (error) {
console.error(`Error fetching data from ${url}: ${error}`);
yield { error: error.message, url }; // Yield an error object
}
}
async function main() {
const generator = safeFetch('https://api.example.com/data'); // Replace with an actual URL, but make sure it exists to test
for await (const result of generator) {
if (result.error) {
console.log(`Failed to fetch data from ${result.url}: ${result.error}`);
} else {
console.log('Fetched data:', result);
}
}
}
main();
در این مثال، مولد safeFetch هرگونه خطایی را که در طول عملیات fetch رخ می دهد، میگیرد و یک شی خطا را بازده میدهد. کد فراخوانی سپس می تواند وجود یک خطا را بررسی و بر اساس آن مدیریت کند.
مثالها و موارد استفاده عملی
در اینجا برخی از نمونه ها و موارد استفاده عملی وجود دارد که در آن هماهنگی چند مولد می تواند مفید باشد:
- جریان داده: پردازش مجموعه داده های بزرگ در تکه ها با استفاده از مولدها، با چندین مولد که تبدیل های مختلفی را به طور همزمان بر روی جریان داده انجام می دهند. تصور کنید که یک فایل log بسیار بزرگ را پردازش می کنید: یک مولد ممکن است فایل را بخواند، دیگری ممکن است خطوط را تجزیه کند، و سومی ممکن است آمار را جمع کند.
- پردازش داده در زمان واقعی: رسیدگی به جریان های داده در زمان واقعی از منابع متعدد، مانند حسگرها یا تیکرهای سهام، با استفاده از مولدها برای فیلتر کردن، تبدیل و تجمیع داده ها.
- ارکستراسیون ریزخدمات: هماهنگ کردن تماس ها با چندین ریزخدمات با استفاده از مولدها، که هر مولد نشان دهنده تماسی با یک سرویس متفاوت است. این می تواند گردشهای کاری پیچیده را که شامل تعامل بین چندین سرویس هستند ساده کند. به عنوان مثال، یک سیستم پردازش سفارش تجارت الکترونیکی ممکن است شامل تماس با یک سرویس پرداخت، یک سرویس موجودی و یک سرویس حمل و نقل باشد.
- توسعه بازی: پیاده سازی منطق پیچیده بازی با استفاده از مولدها، با چندین مولد که جنبه های مختلف بازی، مانند هوش مصنوعی، فیزیک و رندر را کنترل می کنند.
- فرایندهای ETL (Extract, Transform, Load): ساده سازی خطوط لوله ETL با استفاده از توابع مولد برای استخراج داده ها از منابع مختلف، تبدیل آن به فرمت دلخواه و بارگذاری آن در یک پایگاه داده یا انبار داده هدف. هر مرحله (Extract, Transform, Load) می تواند به عنوان یک مولد جداگانه پیاده سازی شود، که امکان کد ماژولار و قابل استفاده مجدد را فراهم می کند.
مزایای استفاده از توابع مولد برای ترکیب Async
- خوانایی بهبود یافته: کد ناهمزمان نوشته شده با مولدها می تواند خواناتر و درک آن آسان تر از کد نوشته شده با تماسهای برگشتی یا Promise ها باشد.
- مدیریت خطای ساده شده: توابع مولد با اجازه دادن به شما برای استفاده از بلوک های
try...catchبرای گرفتن خطاهایی که در طول عملیات ناهمزمان رخ می دهند، مدیریت خطا را ساده می کنند. - افزایش قابلیت ترکیب: توابع مولد بسیار قابل ترکیب هستند و به شما این امکان را می دهند که به راحتی چندین مولد را برای ایجاد گردشهای کاری ناهمزمان پیچیده ترکیب کنید.
- قابلیت نگهداری پیشرفته: ماژولار بودن و قابلیت ترکیب توابع مولد باعث می شود کد آسان تر نگهداری و به روز شود.
- قابلیت آزمایش بهبود یافته: توابع مولد آسان تر از کد نوشته شده با تماسهای برگشتی یا Promise ها تست می شوند، زیرا می توانید به راحتی جریان اجرا را کنترل کرده و عملیات ناهمزمان را شبیه سازی کنید.
چالش ها و ملاحظات
- منحنی یادگیری: درک توابع مولد می تواند پیچیده تر از تکنیک های سنتی برنامه نویسی ناهمزمان باشد.
- اشکال زدایی: اشکال زدایی ترکیب مولد ناهمزمان می تواند چالش برانگیز باشد، زیرا ردیابی جریان اجرا می تواند دشوار باشد. استفاده از روشهای ورود به سیستم خوب بسیار مهم است.
- عملکرد: در حالی که مولدها مزایای خوانایی را ارائه می دهند، استفاده نادرست می تواند منجر به گلوگاه های عملکرد شود. از سربار تعویض زمینه بین مولدها آگاه باشید، به ویژه در برنامه های کاربردی که عملکرد حیاتی است.
- پشتیبانی مرورگر: در حالی که مرورگرهای مدرن به طور کلی از توابع مولد به خوبی پشتیبانی می کنند، در صورت لزوم از سازگاری با مرورگرهای قدیمی اطمینان حاصل کنید.
- سربار: مولدها در مقایسه با async/await سنتی به دلیل تعویض زمینه، سربار کمی دارند. اگر عملکرد در برنامه شما حیاتی است، آن را اندازه گیری کنید.
بهترین روش ها
- مولدها را کوچک و متمرکز نگه دارید: هر مولد باید یک کار واحد و تعریف شده را انجام دهد. این خوانایی و قابلیت نگهداری را بهبود می بخشد.
- از نام های توصیفی استفاده کنید: از نام ها و متغیرهای واضح و توصیفی برای توابع مولد خود استفاده کنید.
- کد خود را مستند کنید: کد خود را به طور کامل مستند کنید و هدف هر مولد و نحوه تعامل آن با مولدهای دیگر را توضیح دهید.
- کد خود را آزمایش کنید: کد خود را به طور کامل، از جمله تست های واحد و تست های یکپارچه سازی، آزمایش کنید.
- از لینترها و قالببندهای کد استفاده کنید: از لینترها و قالببندهای کد برای اطمینان از سازگاری و کیفیت کد استفاده کنید.
- استفاده از یک کتابخانه را در نظر بگیرید: کتابخانه هایی مانند co یا iter-tools ابزارهایی را برای کار با مولدها ارائه می دهند و می توانند وظایف رایج را ساده کنند.
نتیجه
توابع مولد جاوااسکریپت، در صورت ترکیب با تکنیک های برنامه نویسی ناهمزمان، یک رویکرد قدرتمند و انعطاف پذیر برای مدیریت گردشهای کاری ناهمزمان پیچیده ارائه می دهند. با تسلط بر تکنیکهای ترکیب و هماهنگسازی چندین مولد، میتوانید کد تمیزتر، قابل مدیریتتر و قابل نگهداریتری ایجاد کنید. در حالی که چالشها و ملاحظاتی وجود دارد که باید از آنها آگاه بود، مزایای استفاده از توابع مولد برای ترکیب async اغلب از معایب آن بیشتر است، به ویژه در برنامههای پیچیدهای که نیاز به هماهنگی بین چندین منبع داده ناهمزمان یا مراحل پردازش دارند. با تکنیک های شرح داده شده در این پست آزمایش کنید و قدرت هماهنگی چند مولد را در پروژه های خود کشف کنید.